To reduce the risk of cross-site scripting attacks, templating systems, such as Twig
, Django
, Smarty
,
Groovy's template engine
, allow configuration of automatic variable escaping before rendering templates. When escape occurs, characters
that make sense to the browser (eg: <a>) will be transformed/replaced with escaped/sanitized values (eg: & lt;a& gt; ).
Auto-escaping is not a magic feature to annihilate all cross-site scripting attacks, it depends on the strategy applied and the context, for example a "html auto-escaping" strategy
(which only transforms html characters into html entities) will not be relevant
when variables are used in a html attribute because ':
' character is not
escaped and thus an attack as below is possible:
<a href="{{ myLink }}">link</a> // myLink = javascript:alert(document.cookie)
<a href="javascript:alert(document.cookie)">link</a> // JS injection (XSS attack)
Ask Yourself Whether
- Templates are used to render web content and
- dynamic variables in templates come from untrusted locations or are user-controlled inputs
- there is no local mechanism in place to sanitize or validate the inputs.
There is a risk if you answered yes to any of those questions.
Recommended Secure Coding Practices
Enable auto-escaping by default and continue to review the use of inputs in order to be sure that the chosen auto-escaping strategy is the right
one.
Sensitive Code Example
mustache.js template engine:
let Mustache = require("mustache");
Mustache.escape = function(text) {return text;}; // Sensitive
let rendered = Mustache.render(template, { name: inputName });
handlebars.js template engine:
const Handlebars = require('handlebars');
let source = "<p>attack {{name}}</p>";
let template = Handlebars.compile(source, { noEscape: true }); // Sensitive
markdown-it markup language parser:
const markdownIt = require('markdown-it');
let md = markdownIt({
html: true // Sensitive
});
let result = md.render('# <b>attack</b>');
marked markup language parser:
const marked = require('marked');
marked.setOptions({
renderer: new marked.Renderer(),
sanitize: false // Sensitive
});
console.log(marked("# test <b>attack/b>"));
kramed markup language parser:
let kramed = require('kramed');
var options = {
renderer: new kramed.Renderer({
sanitize: false // Sensitive
})
};
Compliant Solution
mustache.js template engine:
let Mustache = require("mustache");
let rendered = Mustache.render(template, { name: inputName }); // Compliant autoescaping is on by default
handlebars.js template engine:
const Handlebars = require('handlebars');
let source = "<p>attack {{name}}</p>";
let data = { "name": "<b>Alan</b>" };
let template = Handlebars.compile(source); // Compliant by default noEscape is set to false
markdown-it markup language parser:
let md = require('markdown-it')(); // Compliant by default html is set to false
let result = md.render('# <b>attack</b>');
marked markup language parser:
const marked = require('marked');
marked.setOptions({
renderer: new marked.Renderer()
}); // Compliant by default sanitize is set to true
console.log(marked("# test <b>attack/b>"));
kramed markup language parser:
let kramed = require('kramed');
let options = {
renderer: new kramed.Renderer({
sanitize: true // Compliant
})
};
console.log(kramed('Attack [xss?](javascript:alert("xss")).', options));
See